﻿using System;
using System.IO;
using System.Net;
using System.Security;
using System.Timers;

namespace FtpLibDotNet
{
    public class FtpClient : IDisposable
    {
        public ConnectionStatus Status { get; private set; }
        public string FtpHost { get; private set; }
        public int FtpPort { get; private set; }
        public FtpDirectoryInfo CurrentDirectory { get; private set; }

        private FtpConnection connection;
        private string ftpUsername;
        private SecureString ftpPassword;
        private Timer connectionKeeperTimer;

        public FtpClient(string host, int port = 21)
        {
            this.FtpHost = host;
            this.FtpPort = port;
            this.Status = ConnectionStatus.Disconected;
        }

        public FtpClient(string host, int port, NetworkCredential credential)
            : this(host, port, credential.UserName, credential.SecurePassword)
        {
        }

        public FtpClient(string host, string username, SecureString password)
            : this(host, 21, username, password)
        {
        }

        public FtpClient(string host, int port, string username, SecureString password)
            : this(host, port)
        {
            this.ftpUsername = username;
            this.ftpPassword = password.Copy();
        }

        public FtpClient(string host, string username, string password = "")
            : this(host, 21, username, password)
        {
        }

        public FtpClient(string host, int port, string username, string password = "")
            : this(host, port)
        {
            this.ftpUsername = username;
            this.ftpPassword = password.ConvertToSecureString();
        }

        internal FtpClient(FtpConnection connection)
        {
            this.connection = connection;
        }

        public void Open()
        {
            if (this.connection != null && this.connection.Connected)
                throw new FtpException("Client is already connected.");

            // Connect
            this.Status = ConnectionStatus.Connecting;
            this.connection = new FtpConnection(this.FtpHost, this.FtpPort);
            this.connection.ConnectionClosed += this.ConnectionClosed;
            this.connection.Open();

            // Authenticate
            this.Status = ConnectionStatus.Authenticating;
            FtpResponse userResponse = this.connection.SendCommand(FtpCommands.USER(this.ftpUsername));
            if (userResponse.Code != 331 && userResponse.Code != 230)
            {
                this.connection.Close();
                throw new FtpCommandException(userResponse, "Could not connect to remote server. Wrong username.");
            }
            if (userResponse.Code != 230)
            {
                FtpResponse passResponse = this.connection.SendCommand(FtpCommands.PASS(this.ftpPassword));
                if (passResponse.Code != 230 && passResponse.Code != 202)
                {
                    this.connection.Close();
                    throw new FtpCommandException(passResponse, "Could not connect to remote server. Wrong username or password.");
                }
            }

            // Set timer every 2min send empty command to keep connection
            this.connectionKeeperTimer = new Timer(120000.0);
            this.connectionKeeperTimer.AutoReset = true;
            this.connectionKeeperTimer.Elapsed += this.ConnectionKeeperTimerElapsed;
            this.connectionKeeperTimer.Start();

            this.Status = ConnectionStatus.Ready;
        }

        private FtpResponse SendCommand(string command)
        {
            ConnectionStatus originalStatus = this.Status;
            this.Status = ConnectionStatus.Busy;
            // Reset keeper timer (?)
            FtpResponse response = this.connection.SendCommand(command);
            this.Status = originalStatus;
            return response;
        }

        public void Close()
        {
            if (this.connectionKeeperTimer != null && this.connectionKeeperTimer.Enabled)
                this.connectionKeeperTimer.Stop();

            if (this.connection != null && this.connection.Connected)
                this.connection.Close();

            this.Status = ConnectionStatus.Disconected;
        }

        protected void ConnectionClosed(object sender, EventArgs e)
        {
            if (sender.Equals(this.connection))
                this.Status = ConnectionStatus.Disconected;
        }

        protected void ConnectionKeeperTimerElapsed(object sender, ElapsedEventArgs e)
        {
            if (this.connection != null && this.connection.Connected)
                this.connection.SendCommand(FtpCommands.NOOP());
        }

        void IDisposable.Dispose()
        {
            if (this.Status != ConnectionStatus.Disconected)
                this.Close();
            this.connectionKeeperTimer.Stop();
        }

        #region Directory Commands

        public FtpDirectoryInfo GetCurrentDirectory()
        {
            FtpResponse response = this.SendCommand(FtpCommands.PWD());
            string path = response.Data.Split('"')[1];
            this.CurrentDirectory = FtpDirectoryInfo.Create(this.connection, path);
            return this.CurrentDirectory;
        }

        public FtpDirectoryInfo ChangeCurrentDirectory(string path)
        {
            FtpResponse response = this.SendCommand(FtpCommands.CWD(path));
            if (response.Code != 250)
                throw new FtpCommandException(response, "Could not change working directory.");
            return this.GetCurrentDirectory();
        }

        public FtpDirectoryInfo ChangeCurrentDirectoryUp()
        {
            FtpResponse response = this.SendCommand(FtpCommands.CDUP());
            if (response.Code != 250)
                throw new FtpCommandException(response, "Could not change working directory.");
            return this.GetCurrentDirectory();
        }

        public void CreateDirectory(string path)
        {
            FtpResponse response = this.SendCommand(FtpCommands.MKD(path));
            if (response.Code != 250 && response.Code != 257)
                throw new FtpCommandException(response, "Could not create new directory.");
        }

        public void RenameDirectory(string originalPath, string newPath)
        {
            FtpResponse fromResponse = this.SendCommand(FtpCommands.RNFR(originalPath));
            if (fromResponse.Code != 350)
                throw new FtpCommandException(fromResponse, "Could not rename directory.");

            FtpResponse toResponse = this.SendCommand(FtpCommands.RNTO(newPath));
            if (toResponse.Code != 250)
                throw new FtpCommandException(toResponse, "Could not rename directory.");
        }

        public void DeleteDirectory(string path)
        {
            FtpResponse response = this.SendCommand(FtpCommands.RMD(path));
            if (response.Code != 250 && response.Code != 257)
                throw new FtpCommandException(response, "Directory could not be deleted.");
        }

        #endregion

        #region File Commands

        public void UploadFile(string localFileName, string remoteFileName)
        {
            FileInfo fi = new FileInfo(localFileName);
            if (!fi.Exists) throw new FtpException("Local file doesn't exists.");
            this.UploadFile(fi.OpenRead(), remoteFileName);
        }

        public FtpFileInfo UploadFile(Stream stream, string remoteFileName)
        {
            return null;
        }

        public void DownloadFile(string remoteFileName, string localFileName, bool overwriteExisting = true)
        {
            FileInfo fi = new FileInfo(localFileName);
            if (!overwriteExisting && fi.Exists) throw new FtpException("Local file already exists.");
            this.DownloadFile(remoteFileName, fi.OpenWrite());
        }

        public void DownloadFile(string remoteFileName, Stream stream)
        {

        }

        public void RenameFile(string originalFileName, string newFileName)
        {
            FtpResponse fromResponse = this.SendCommand(FtpCommands.RNFR(originalFileName));
            if (fromResponse.Code != 350)
                throw new FtpCommandException(fromResponse, "Could not rename directory.");

            FtpResponse toResponse = this.SendCommand(FtpCommands.RNTO(newFileName));
            if (toResponse.Code != 250)
                throw new FtpCommandException(toResponse, "Could not rename directory.");
        }

        public void DeleteFile(string fileName)
        {
            FtpResponse response = this.SendCommand(FtpCommands.DELE(fileName));
            if (response.Code != 250)
                throw new FtpCommandException(response, "File could not be deleted.");
        }

        #endregion
    }
}